/*
 * Copyright 2020-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.oauth2.server.authorization.authentication;

import java.security.Principal;
import java.util.Base64;
import java.util.Set;
import java.util.function.Consumer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request
 * used in the Authorization Code Grant.
 *
 * @author Joe Grandja
 * @author Steve Riesenberg
 * @since 0.1.2
 * @see OAuth2AuthorizationCodeRequestAuthenticationToken
 * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
 * @see OAuth2AuthorizationCodeAuthenticationProvider
 * @see OAuth2AuthorizationConsentAuthenticationProvider
 * @see RegisteredClientRepository
 * @see OAuth2AuthorizationService
 * @see OAuth2AuthorizationConsentService
 * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
 */
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
	private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
			new Base64StringKeyGenerator(Base64.getUrlEncoder());
	private final Log logger = LogFactory.getLog(getClass());
	// 注册的client的repo
	private final RegisteredClientRepository registeredClientRepository;
	// OAuth2的Authentication的信息，默认InMemory实现
	// 包含两个部分，一个已经完成，一个流程中
	//	Map<String, OAuth2Authorization> initializedAuthorizations = ...
	//	Map<String, OAuth2Authorization> authorizations = ...
	private final OAuth2AuthorizationService authorizationService;
	// OAuth2已经授权过的信息
	private final OAuth2AuthorizationConsentService authorizationConsentService;
	// code生成器
	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
			new OAuth2AuthorizationCodeRequestAuthenticationValidator();

	/**
	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
	 *
	 * @param registeredClientRepository the repository of registered clients
	 * @param authorizationService the authorization service
	 * @param authorizationConsentService the authorization consent service
	 */
	public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
			OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
		Assert.notNull(authorizationService, "authorizationService cannot be null");
		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
		this.registeredClientRepository = registeredClientRepository;
		this.authorizationService = authorizationService;
		this.authorizationConsentService = authorizationConsentService;
	}
	//注意这里在往oauth2_authorization表存数据的时候，将当前登录的用户信息principal也存入了该表中的attributes字段中
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
		// 根据clientId获取客户端注册信息(初始化时存入到了数据库，此处从数据库中获取)
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
				authorizationCodeRequestAuthentication.getClientId());
		// 没有就抛异常
		if (registeredClient == null) {
			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
					authorizationCodeRequestAuthentication, null);
		}

		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Retrieved registered client");
		}
		// 有client进一步校验参数（主要是RedirectUri 维护在表中的客户端对应的重定向地址跟request中的是否一致）
		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext =
				OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication)
						.registeredClient(registeredClient)
						.build();
		this.authenticationValidator.accept(authenticationContext);
		// 注册的客户端不支持code grant的类型，抛异常
		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
					authorizationCodeRequestAuthentication, registeredClient);
		}

		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)客户端none模式时会传递该参数，进而存放到authorizationCodeRequestAuthentication.getAdditionalParameters()集合中
		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
		//code_challenge不为空，获取code_challenge_method参数从authentication中
		if (StringUtils.hasText(codeChallenge)) {
			String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
			//如果code_challenge_method为空或者不等于S256则抛出异常，客户端传递时写死的也是S256
			if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
				throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
						authorizationCodeRequestAuthentication, registeredClient, null);
			}
		// code_challenge为空，判断registeredClient.getClientSettings().isRequireProofKey()是否为true，是则抛出异常。
		// 为true说明如果客户端在执行授权码授予流程时需要提供证明密钥质询（code_challenge）和验证者（code_challenge_method），默认值为false 。
		} else if (registeredClient.getClientSettings().isRequireProofKey()) {
			throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
					authorizationCodeRequestAuthentication, registeredClient, null);
		}

		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Validated authorization code request parameters");
		}

		// ---------------
		// The request is valid - ensure the resource owner is authenticated
		// ---------------
		// 获得当前的Authentication,此时类型是OAuth2AuthorizationCodeRequestAuthenticationToken，里面的principal
		// 在构建该对象的时候从安全上下文获取(已认证)，如果获取不到则赋值匿名用户(已认证)
		// 注意:principal里的认证属性跟authorizationCodeRequestAuthentication对象的是两个
		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
		// 没有认证过或者匿名用户，直接返回，未登录时客户端第一次授权请求开始就在这里返回进而跳转到登录页面
		if (!isPrincipalAuthenticated(principal)) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Did not authenticate authorization code request since principal not authenticated");
			}
			// Return the authorization request as-is where isAuthenticated() is false
			// 如果此处直接返回意味着principal是匿名对象未进行认证(需要注意的是此时authorizationCodeRequestAuthentication对象中的authenticated属性为false)
			return authorizationCodeRequestAuthentication;
		}

		// 进行到这里，表示用户已经通过登录执行链的SecurityFilterChain登录过，并返回了登录信息
		// 登录后下面即将开始授权流程

		// 生成一个Request包含前面所有信息 封装了客户端授权请求的所有参数信息
		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
				.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
				.clientId(registeredClient.getClientId())
				.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
				.scopes(authorizationCodeRequestAuthentication.getScopes())
				.state(authorizationCodeRequestAuthentication.getState())
				.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
				.build();
		// 根据client_id和name获取内存或者数据库表（oauth2_authorization_consent{记录用户确认授权记录}）对应的数据OAuth2AuthorizationConsent，没有授权这里为null
		// 同时需要注意再OAuth2AuthorizationConsentAuthenticationProvider授权完成后会拼接一个OAuth2AuthorizationConsent对象将对应的数据存入
		// oauth2_authorization_consent表中 等该客户端(相同登录用户)再次请求授权操作时此时currentAuthorizationConsent便有数据了
		// 换句话说再范围包括的前提下一次授权终生使用，范围如果不包括还需要授权，授权完成后会更新该范围(合并更新)
		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
				registeredClient.getId(), principal.getName());
		// requireAuthorizationConsent方法 判断是否需要进行授权操作，以下情况均不需要授权页面
		// 1:oauth2_registered_client表中的require-authorization-consent值为false--AuthorizationServiceConfig配置类中可进行定义
		// 2:require-authorization-consent值为true但是请求中的scope只包含一个openid
		// 3:require-authorization-consent值为true且(请求中的scope含有多个值或者有一个值但不是openid) oauth2_authorization_consent表中记录的
		// 用户访问该客户端的authorities全下包含请求中的scope  --oauth2_authorization_consent用来记录给某个用户访问某个客户端时的权限，如果访问的权限都在表中记录着说明不需要授权了
		if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
			// 需要授权 重新生成state
			String state = DEFAULT_STATE_GENERATOR.generateKey();
			// 此处将authorizationRequest以一个属性字段存入了oauth2_authorization表中，
			// 以便再之后的授权流程(OAuth2AuthorizationConsentAuthenticationProvider)中获取客户端发起授权请求时携带的一些参数信息
			OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
					.attribute(OAuth2ParameterNames.STATE, state)
					.build();

			if (this.logger.isTraceEnabled()) {
				logger.trace("Generated authorization consent state");
			}
			// 将OAuth2Authorization实例保存到内存或者数据库表（oauth2_authorization）中  注意此处OAuth2Authorization对象数据是不全的，
			// 没有code等信息，多了一个state字段  会在OAuth2AuthorizationConsentAuthenticationProvider中进行数据的更新 填充code信息，state置空
			this.authorizationService.save(authorization);
            // 默认为null 代表着已经给该用户授权了哪些范围
			Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
					currentAuthorizationConsent.getScopes() : null;

			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Saved authorization");
			}
			// 返回OAuth2AuthorizationConsentAuthenticationToken类型 注意状态为已认证
			// 在OAuth2AuthenticationEndpointFilter中会判断返回结果Authentication是否是下面的类型
			// 如果是表示需要进行授权操作，授权重定向流程在OAuth2AuthenticationEndpointFilter#sendAuthorizationConsent中
			return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
					registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
		}
		// 此时已经授权通过了，根据数据库查询的客户端数据和请求的参数数据封装构建tokenContext对象
		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
				authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());
		// 生成授权码
		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
		// 如果授权码生成为null，则抛出异常
		if (authorizationCode == null) {
			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
					"The token generator failed to generate the authorization code.", ERROR_URI);
			throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
		}

		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Generated authorization code");
		}
        //根据上面生成的数据构建authorization对象
		OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
				.authorizedScopes(authorizationRequest.getScopes())
				.token(authorizationCode)
				.build();
		// 对OAuth2Authorization进行保存或更新操作。保存或更新到内存或数据库表（oauth2_authorization{记录发放令牌记录}）中。如果是保存到表中的是先根据Id查询记录是否存在，
		// 但上面构建的OAuth2Authorization的ID是通过UUID.randomUUID().toString()生成的所以很少存在重复的记录，也就是说对OAuth2Authorization的操作基本上为插入操作
		this.authorizationService.save(authorization);

		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Saved authorization");
		}
        //判断请求中的redirectUri是否为空，如果为空获取registeredClient(数据库)的redirectUri作为转发地址
		String redirectUri = authorizationRequest.getRedirectUri();
		if (!StringUtils.hasText(redirectUri)) {
			redirectUri = registeredClient.getRedirectUris().iterator().next();
		}

		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Authenticated authorization code request");
		}
		// 构建OAuth2AuthorizationCodeRequestAuthenticationToken返回 注意已认证
		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
				registeredClient.getClientId(), principal, authorizationCode, redirectUri,
				authorizationRequest.getState(), authorizationRequest.getScopes());
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
	}

	/**
	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
	 *
	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
	 * @since 0.2.3
	 */
	public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
		this.authorizationCodeGenerator = authorizationCodeGenerator;
	}

	/**
	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
	 * and is responsible for validating specific OAuth 2.0 Authorization Request parameters
	 * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
	 * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
	 *
	 * <p>
	 * <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
	 *
	 * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
	 * @since 0.4.0
	 */
	public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
		this.authenticationValidator = authenticationValidator;
	}

	private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
			OAuth2AuthorizationRequest authorizationRequest) {
		return OAuth2Authorization.withRegisteredClient(registeredClient)
				.principalName(principal.getName())
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.attribute(Principal.class.getName(), principal)
				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
	}

	private static OAuth2TokenContext createAuthorizationCodeTokenContext(
			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
			RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {

		// @formatter:off
		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
				.registeredClient(registeredClient)
				.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
				.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
				.authorizedScopes(authorizedScopes)
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrant(authorizationCodeRequestAuthentication);
		// @formatter:on

		if (authorization != null) {
			tokenContextBuilder.authorization(authorization);
		}

		return tokenContextBuilder.build();
	}
    //registeredClient:数据库oauth2_registered_client  authorizationRequest:请求传递数据  authorizationConsent:数据库oauth2_authorization_consent
	private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
			OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
		//检验registeredClient的ClientSettings中require-authorization-consent值是否为true
		if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
			return false;
		}
		// 'openid' scope does not require consent
		//OAuth2AuthorizationRequest的scopes中有且只包含openid
		if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
				authorizationRequest.getScopes().size() == 1) {
			return false;
		}
		//OAuth2AuthorizationConsent不为空而且OAuth2AuthorizationConsent的scopes(对应表中的authorities字段)包含OAuth2AuthorizationRequest中的scopes
		if (authorizationConsent != null &&
				authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
			return false;
		}

		return true;
	}

	private static boolean isPrincipalAuthenticated(Authentication principal) {
		return principal != null &&
				!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
				principal.isAuthenticated();
	}

	private static void throwError(String errorCode, String parameterName,
			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
			RegisteredClient registeredClient) {
		throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null);
	}

	private static void throwError(String errorCode, String parameterName, String errorUri,
			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
		throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
	}

	private static void throwError(OAuth2Error error, String parameterName,
			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {

		String redirectUri = resolveRedirectUri(authorizationCodeRequestAuthentication, authorizationRequest, registeredClient);
		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
				(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
						parameterName.equals(OAuth2ParameterNames.STATE))) {
			redirectUri = null;		// Prevent redirects
		}

		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
				new OAuth2AuthorizationCodeRequestAuthenticationToken(
						authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
						(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
						authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
						authorizationCodeRequestAuthentication.getAdditionalParameters());

		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
	}

	private static String resolveRedirectUri(
			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
			OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {

		if (authorizationCodeRequestAuthentication != null && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
			return authorizationCodeRequestAuthentication.getRedirectUri();
		}
		if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
			return authorizationRequest.getRedirectUri();
		}
		if (registeredClient != null) {
			return registeredClient.getRedirectUris().iterator().next();
		}
		return null;
	}

}
